/*
 * Copyright (c) 2025 Huawei Technologies Co., Ltd.
 * openFuyao is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *          http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */

package phases

import (
	"context"
	"fmt"
	"strings"

	"github.com/coreos/go-semver/semver"
	confv1beta1 "gopkg.openfuyao.cn/bkecommon/cluster/api/v1beta1"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/util/retry"
	ctrl "sigs.k8s.io/controller-runtime"

	bkev1beta1 "gopkg.openfuyao.cn/cluster-api-provider-bke/api/v1beta1"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/pkg/kube"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/pkg/phaseframe"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/pkg/phaseframe/phaseutil"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/utils"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/utils/constant"
)

const (
	EnsureComponentUpgradeName confv1beta1.BKEClusterPhase = "EnsureComponentUpgrade"
)

type EnsureComponentUpgrade struct {
	phaseframe.BasePhase
	localKubeConfig []byte
	remoteClient    *kubernetes.Clientset
}

func NewEnsureComponentUpgrade(ctx *phaseframe.PhaseContext) phaseframe.Phase {
	base := phaseframe.NewBasePhase(ctx, EnsureComponentUpgradeName)
	return &EnsureComponentUpgrade{BasePhase: base}
}

// Execute 执行具体的升级操作
func (e *EnsureComponentUpgrade) Execute() (ctrl.Result, error) {
	if err := e.getRemoteClient(); err != nil {
		return ctrl.Result{}, err
	}
	if err := e.loadLocalKubeConfig(); err != nil {
		return ctrl.Result{}, err
	}
	return e.rolloutOpenfuyaoComponent()
}

func (e *EnsureComponentUpgrade) loadLocalKubeConfig() error {
	ctx, c, _, _, log := e.Ctx.Untie()
	localKubeConfig, err := phaseutil.GetLocalKubeConfig(ctx, c)
	if err != nil {
		if apierrors.IsNotFound(err) {
			log.Error(constant.ComponentUpgradeFailed, "Local kubeconfig secret not found")
			return fmt.Errorf("local kubeconfig secret not found")
		}
		log.Error(constant.ComponentUpgradeFailed, "Failed to get local kubeconfig secret, err：%v", err)
		return fmt.Errorf("failed to get local kubeconfig secret, err：%v", err)
	}
	e.localKubeConfig = localKubeConfig
	return nil
}

// getRemoteClient get remote cluster client
func (e *EnsureComponentUpgrade) getRemoteClient() error {
	ctx, c, bkeCluster, _, log := e.Ctx.Untie()
	log.Error(constant.ComponentUpgradingReason, "bke cluster is: %+v", *bkeCluster)
	targetClusterClient, err := kube.NewRemoteClientByBKECluster(ctx, c, bkeCluster)
	if err != nil {
		log.Error(constant.InternalErrorReason, "failed to get BKECluster %q remote cluster client", utils.ClientObjNS(bkeCluster))
		return err
	}
	e.remoteClient, _ = targetClusterClient.KubeClient()
	if e.remoteClient == nil {
		return fmt.Errorf("failed to get remote client")
	}
	return nil
}

func (e *EnsureComponentUpgrade) isPatchVersion(version string) bool {
	cleanVersion := strings.TrimPrefix(version, "v")

	v, err := semver.NewVersion(cleanVersion)
	if err != nil {
		return false
	}

	return v.Patch > 0 && v.PreRelease == ""
}

func (e *EnsureComponentUpgrade) isComponentNeedUpgrade(old *bkev1beta1.BKECluster, new *bkev1beta1.BKECluster) bool {
	// 初次安装，如果安装的是补丁版本，那么就需要进行openFuyao核心组件的升级处理
	if new.Status.OpenFuyaoVersion == "" {
		return e.isPatchVersion(new.Spec.ClusterConfig.Cluster.OpenFuyaoVersion)
	}
	// 非初次安装，需要根据status和spec中的openFuyao版本判断是否需要进行核心组件的升级处理
	nodes := phaseutil.GetNeedUpgradeNodes(new)
	if nodes == nil || nodes.Length() == 0 {
		return false
	}

	return true
}

// NeedExecute 这个阶段，只有在初始新建补丁版本时才需要执行，如何判断是初始新建补丁版本？old为空，new中openFuyao version带小版本
func (e *EnsureComponentUpgrade) NeedExecute(old *bkev1beta1.BKECluster, new *bkev1beta1.BKECluster) bool {
	if !e.BasePhase.DefaultNeedExecute(old, new) {
		return false
	}
	if !e.isComponentNeedUpgrade(old, new) {
		return false
	}
	e.SetStatus(bkev1beta1.PhaseWaiting)
	return true
}

func (e *EnsureComponentUpgrade) rolloutOpenfuyaoComponent() (ctrl.Result, error) {
	_, _, bkeCluster, _, log := e.Ctx.Untie()
	patchCfg, err := e.getPatchConfig()
	if err != nil {
		return ctrl.Result{}, err
	}

	if err = e.processImageUpdates(patchCfg); err != nil {
		return ctrl.Result{}, err
	}

	log.Info(constant.ComponentUpgradeSuccess, "upgrade all component success")
	bkeCluster.Status.OpenFuyaoVersion = bkeCluster.Spec.ClusterConfig.Cluster.OpenFuyaoVersion

	return ctrl.Result{}, nil
}

func (e *EnsureComponentUpgrade) getPatchConfig() (*phaseutil.PatchConfig, error) {
	ctx, c, bkeCluster, _, log := e.Ctx.Untie()
	openFuyaoVersion := bkeCluster.Spec.ClusterConfig.Cluster.OpenFuyaoVersion
	log.Info(constant.ComponentUpgradingReason, "openFuyaoVersion: %v", openFuyaoVersion)

	localConfigMap := &corev1.ConfigMap{}
	if err := c.Get(ctx, constant.GetLocalConfigMapObjectKey(), localConfigMap); err != nil {
		log.Error(constant.InternalErrorReason, "failed to get local cluster bke-config cm, err: %v", err)
		return nil, fmt.Errorf("get cm failed %v", err)
	}

	cmKey := fmt.Sprintf("patchFile.%s", openFuyaoVersion)
	log.Info(constant.ComponentUpgradingReason, "local cluster config map: %+v", localConfigMap)

	if _, ok := localConfigMap.Data[cmKey]; !ok {
		return nil, fmt.Errorf("patchFile %s not found in config", cmKey)
	}

	log.Info(constant.ComponentUpgradingReason, "get patch config file: %v", localConfigMap.Data[cmKey])
	return phaseutil.GetPatchConfig(localConfigMap.Data[cmKey])
}

func (e *EnsureComponentUpgrade) processImageUpdates(patchCfg *phaseutil.PatchConfig) error {
	for _, repo := range patchCfg.Repos {
		if err := e.processRepoImages(repo); err != nil {
			return err
		}
	}
	return nil
}

func (e *EnsureComponentUpgrade) processRepoImages(repo phaseutil.Repo) error {
	if repo.IsKubernetes {
		return nil // k8s组件在其他流程升级
	}

	for _, subImage := range repo.SubImages {
		if err := e.processSubImage(subImage); err != nil {
			return err
		}
	}
	return nil
}

func (e *EnsureComponentUpgrade) processSubImage(subImage phaseutil.SubImage) error {
	for _, image := range subImage.Images {
		if err := e.updateSingleImage(image); err != nil {
			return err
		}
	}
	return nil
}

func (e *EnsureComponentUpgrade) updateSingleImage(image phaseutil.Image) error {
	_, _, _, _, log := e.Ctx.Untie()
	// 依次处理镜像，补丁升级操作确保只会有一个tag
	if len(image.Tag) == 0 {
		return fmt.Errorf("image %s has no tags", image.Name)
	}

	tag := image.Tag[0]
	for _, podInfo := range image.UsedPodInfo {
		updateInfo := &phaseutil.ImageUpdate{
			ImageName: image.Name,
			PodPrefix: podInfo.PodPrefix,
			NameSpace: podInfo.NameSpace,
			NewTag:    tag,
		}
		log.Error(constant.ComponentUpgradeFailed, "update info is %+v", updateInfo)
		if err := e.updatePodImageTag(updateInfo); err != nil {
			log.Error(constant.ComponentUpgradeFailed, "update image %s tag failed, err: %v", image.Name, err)
			return err
		}
	}

	return nil
}

func (e *EnsureComponentUpgrade) updatePodImageTag(update *phaseutil.ImageUpdate) error {
	pods, err := e.findMatchingPods(update.NameSpace, update.PodPrefix)
	if err != nil {
		return fmt.Errorf("failed to find matching %s/%s pods: %v", update.NameSpace, update.PodPrefix, err)
	}

	if len(pods) == 0 {
		return fmt.Errorf("no pods found in %s with prefix %s", update.NameSpace, update.PodPrefix)
	}
	return e.upgradePodImage(pods[0], update)
}

func (e *EnsureComponentUpgrade) findMatchingPods(namespace, podPrefix string) ([]corev1.Pod, error) {
	ctx, _, _, _, _ := e.Ctx.Untie()
	pods, err := e.remoteClient.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{})
	if err != nil {
		return nil, err
	}
	var matchingPods []corev1.Pod
	for _, pod := range pods.Items {
		if strings.HasPrefix(pod.Name, podPrefix) {
			matchingPods = append(matchingPods, pod)
		}
	}

	return matchingPods, nil
}

func (e *EnsureComponentUpgrade) upgradePodImage(pod corev1.Pod, update *phaseutil.ImageUpdate) error {
	_, _, _, _, log := e.Ctx.Untie()
	controller, controllerType, err := e.getPodController(pod)
	if err != nil {
		return fmt.Errorf("failed to get controller for pod %s: %v", pod.Name, err)
	}
	log.Error(constant.ComponentUpgradingReason, "Pod %s is managed by %s: %s", pod.Name, controllerType, controller.GetName())

	switch controllerType {
	case "Deployment":
		return e.upgradeDeploymentImage(controller.(*appsv1.Deployment), update)
	case "StatefulSet":
		return e.upgradeStatefulSetImage(controller.(*appsv1.StatefulSet), update)
	case "DaemonSet":
		return e.upgradeDaemonSetImage(controller.(*appsv1.DaemonSet), update)
	case "ReplicaSet":
		return e.upgradeReplicaSetImage(controller.(*appsv1.ReplicaSet), update)
	default:
		return fmt.Errorf("unsupported controller type: %s", controllerType)
	}
}

func (e *EnsureComponentUpgrade) getPodController(pod corev1.Pod) (metav1.Object, string, error) {
	ctx, _, _, _, _ := e.Ctx.Untie()
	namespace := e.getNamespace(pod)

	for _, ownerRef := range pod.OwnerReferences {
		controller, kind, err := e.handleOwnerReference(ctx, e.remoteClient, namespace, ownerRef)
		if err != nil {
			return nil, "", err
		}
		if controller != nil {
			return controller, kind, nil
		}
	}

	return &pod, "Pod", nil
}

func (e *EnsureComponentUpgrade) getNamespace(pod corev1.Pod) string {
	if pod.Namespace == "" {
		return metav1.NamespaceDefault
	}
	return pod.Namespace
}

func (e *EnsureComponentUpgrade) handleOwnerReference(ctx context.Context, clientSet kubernetes.Interface, namespace string, ownerRef metav1.OwnerReference) (metav1.Object, string, error) {
	switch ownerRef.Kind {
	case "ReplicaSet":
		return e.handleReplicaSet(ctx, clientSet, namespace, ownerRef)
	case "StatefulSet":
		return e.handleStatefulSet(ctx, clientSet, namespace, ownerRef)
	case "DaemonSet":
		return e.handleDaemonSet(ctx, clientSet, namespace, ownerRef)
	default:
		return nil, "", nil
	}
}

func (e *EnsureComponentUpgrade) handleReplicaSet(ctx context.Context, clientSet kubernetes.Interface, namespace string, ownerRef metav1.OwnerReference) (metav1.Object, string, error) {
	rs, err := clientSet.AppsV1().ReplicaSets(namespace).Get(ctx, ownerRef.Name, metav1.GetOptions{})
	if err != nil {
		return nil, "", err
	}

	for _, rsOwnerRef := range rs.OwnerReferences {
		if rsOwnerRef.Kind == "Deployment" {
			deployment, err := clientSet.AppsV1().Deployments(namespace).Get(ctx, rsOwnerRef.Name, metav1.GetOptions{})
			if err != nil {
				return nil, "", err
			}
			return deployment, "Deployment", nil
		}
	}

	return rs, "ReplicaSet", nil
}

func (e *EnsureComponentUpgrade) handleStatefulSet(ctx context.Context, clientSet kubernetes.Interface, namespace string, ownerRef metav1.OwnerReference) (metav1.Object, string, error) {
	statefulSet, err := clientSet.AppsV1().StatefulSets(namespace).Get(ctx, ownerRef.Name, metav1.GetOptions{})
	if err != nil {
		return nil, "", err
	}
	return statefulSet, "StatefulSet", nil
}

func (e *EnsureComponentUpgrade) handleDaemonSet(ctx context.Context, clientSet kubernetes.Interface, namespace string, ownerRef metav1.OwnerReference) (metav1.Object, string, error) {
	daemonSet, err := clientSet.AppsV1().DaemonSets(namespace).Get(ctx, ownerRef.Name, metav1.GetOptions{})
	if err != nil {
		return nil, "", err
	}
	return daemonSet, "DaemonSet", nil
}

func (e *EnsureComponentUpgrade) isMatchingImage(currentImage, targetImageName string) bool {
	var imageNameWithoutTag string

	lastColonIndex := strings.LastIndex(currentImage, ":")
	if lastColonIndex != -1 {
		slashIndex := strings.LastIndex(currentImage, "/")
		if slashIndex == -1 || lastColonIndex > slashIndex {
			imageNameWithoutTag = currentImage[:lastColonIndex]
		} else {
			imageNameWithoutTag = currentImage
		}
	} else {
		imageNameWithoutTag = currentImage
	}

	return strings.HasSuffix(imageNameWithoutTag, targetImageName)
}

func (e *EnsureComponentUpgrade) buildNewImage(currentImage, newTag string) string {
	lastColonIndex := strings.LastIndex(currentImage, ":")
	if lastColonIndex != -1 {
		slashIndex := strings.LastIndex(currentImage, "/")
		if slashIndex == -1 || lastColonIndex > slashIndex {
			return fmt.Sprintf("%s:%s", currentImage[:lastColonIndex], newTag)
		}
	}
	return fmt.Sprintf("%s:%s", currentImage, newTag)
}

func (e *EnsureComponentUpgrade) upgradeDeploymentImage(deployment *appsv1.Deployment, update *phaseutil.ImageUpdate) error {
	ctx, _, _, _, log := e.Ctx.Untie()
	namespace := deployment.Namespace
	if namespace == "" {
		namespace = metav1.NamespaceDefault
	}
	return retry.RetryOnConflict(retry.DefaultRetry, func() error {
		deploymentCfg, err := e.remoteClient.AppsV1().Deployments(namespace).Get(ctx, deployment.Name, metav1.GetOptions{})
		if err != nil {
			return fmt.Errorf("get deployment err: %s", err)
		}

		needUpdated := false
		for i, container := range deploymentCfg.Spec.Template.Spec.Containers {
			log.Error(constant.ComponentUpgradingReason, "Updating Deployment image info %s: %s -> %s", container.Image, update.ImageName, update.NewTag)
			if e.isMatchingImage(container.Image, update.ImageName) {
				newImage := e.buildNewImage(container.Image, update.NewTag)
				if container.Image != newImage {
					deploymentCfg.Spec.Template.Spec.Containers[i].Image = newImage
					needUpdated = true
					log.Error(constant.ComponentUpgradingReason, "Updating Deployment %s: %s -> %s", deployment.Name, container.Image, newImage)
				}
			}
		}

		if needUpdated {
			_, err = e.remoteClient.AppsV1().Deployments(namespace).Update(ctx, deploymentCfg, metav1.UpdateOptions{})
			return err
		}
		log.Error(constant.ComponentUpgradingReason, "No containers with image '%s' found in Deployment %s", update.ImageName, deployment.Name)
		return nil
	})
}

func (e *EnsureComponentUpgrade) upgradeStatefulSetImage(statefulSet *appsv1.StatefulSet, update *phaseutil.ImageUpdate) error {
	ctx, _, _, _, log := e.Ctx.Untie()
	namespace := statefulSet.Namespace
	if namespace == "" {
		namespace = metav1.NamespaceDefault
	}
	return retry.RetryOnConflict(retry.DefaultRetry, func() error {
		stsCfg, err := e.remoteClient.AppsV1().StatefulSets(namespace).Get(ctx, statefulSet.Name, metav1.GetOptions{})
		if err != nil {
			return err
		}

		needUpdated := false
		for i, container := range stsCfg.Spec.Template.Spec.Containers {
			log.Error(constant.ComponentUpgradingReason, "Updating StatefulSet image info %s: %s -> %s", container.Image, update.ImageName, update.NewTag)
			if e.isMatchingImage(container.Image, update.ImageName) {
				newImage := e.buildNewImage(container.Image, update.NewTag)
				if container.Image != newImage {
					stsCfg.Spec.Template.Spec.Containers[i].Image = newImage
					needUpdated = true
					log.Error(constant.ComponentUpgradingReason, "Updating StatefulSet %s: %s -> %s", statefulSet.Name, container.Image, newImage)
				}
			}
		}

		if needUpdated {
			_, err = e.remoteClient.AppsV1().StatefulSets(namespace).Update(ctx, stsCfg, metav1.UpdateOptions{})
			return err
		}

		log.Error(constant.ComponentUpgradingReason, "No containers with image '%s' found in StatefulSet %s", update.ImageName, statefulSet.Name)
		return nil
	})
}

func (e *EnsureComponentUpgrade) upgradeDaemonSetImage(daemonSet *appsv1.DaemonSet, update *phaseutil.ImageUpdate) error {
	ctx, _, _, _, log := e.Ctx.Untie()
	namespace := daemonSet.Namespace
	if namespace == "" {
		namespace = metav1.NamespaceDefault
	}
	return retry.RetryOnConflict(retry.DefaultRetry, func() error {
		dsCfg, err := e.remoteClient.AppsV1().DaemonSets(namespace).Get(ctx, daemonSet.Name, metav1.GetOptions{})
		if err != nil {
			return err
		}

		dsUpdated := false
		for i, container := range dsCfg.Spec.Template.Spec.Containers {
			log.Error(constant.ComponentUpgradingReason, "Updating DaemonSet image info %s: %s -> %s", container.Image, update.ImageName, update.NewTag)
			if e.isMatchingImage(container.Image, update.ImageName) {
				newImage := e.buildNewImage(container.Image, update.NewTag)
				if container.Image != newImage {
					dsCfg.Spec.Template.Spec.Containers[i].Image = newImage
					dsUpdated = true
					log.Error(constant.ComponentUpgradingReason, "Updating DaemonSet %s: %s -> %s", daemonSet.Name, container.Image, newImage)
				}
			}
		}

		if dsUpdated {
			_, err = e.remoteClient.AppsV1().DaemonSets(namespace).Update(ctx, dsCfg, metav1.UpdateOptions{})
			return err
		}

		log.Error(constant.ComponentUpgradingReason, "No containers with image '%s' found in DaemonSet %s", update.ImageName, daemonSet.Name)
		return nil
	})
}

func (e *EnsureComponentUpgrade) upgradeReplicaSetImage(replicaSet *appsv1.ReplicaSet, update *phaseutil.ImageUpdate) error {
	ctx, _, _, _, log := e.Ctx.Untie()
	namespace := replicaSet.Namespace
	if namespace == "" {
		namespace = metav1.NamespaceDefault
	}
	return retry.RetryOnConflict(retry.DefaultRetry, func() error {
		rsCfg, err := e.remoteClient.AppsV1().ReplicaSets(namespace).Get(ctx, replicaSet.Name, metav1.GetOptions{})
		if err != nil {
			return err
		}

		rsUpdated := false
		for i, container := range rsCfg.Spec.Template.Spec.Containers {
			if e.isMatchingImage(container.Image, update.ImageName) {
				newImage := e.buildNewImage(container.Image, update.NewTag)
				if container.Image != newImage {
					rsCfg.Spec.Template.Spec.Containers[i].Image = newImage
					rsUpdated = true
					log.Error(constant.ComponentUpgradingReason, "Updating ReplicaSet %s: %s -> %s", replicaSet.Name, container.Image, newImage)
				}
			}
		}

		if rsUpdated {
			_, err = e.remoteClient.AppsV1().ReplicaSets(namespace).Update(ctx, rsCfg, metav1.UpdateOptions{})
			return err
		}
		log.Error(constant.ComponentUpgradingReason, "No containers with image '%s' found in ReplicaSet %s", update.ImageName, replicaSet.Name)
		return nil
	})
}
